/*
 * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
 * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
 */

package com.microsoft.sqlserver.jdbc;

import com.microsoft.sqlserver.testframework.AbstractSQLGenerator;
import com.microsoft.sqlserver.testframework.AbstractTest;
import com.microsoft.sqlserver.testframework.Constants;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Random;
import java.util.logging.LogManager;

import static org.junit.Assert.fail;


/**
 * Class for testing maxResultBuffer property, all tests were performed on default connection settings
 * (defaultPacketLength = 8000), only changed were ResponsiveBuffering and MaxResultBuffer
 */
@DisplayName("maxResultBuffer Tests")
public class MaxResultBufferTest extends AbstractTest {

    private static final String TEST_TABLE_NAME = AbstractSQLGenerator
            .escapeIdentifier(RandomUtil.getIdentifier("maxResultBufferTestTable"));
    private static String localConnectionString;

    /**
     * This copies value of localConnectionString from connectionString for each test
     */
    @BeforeEach
    void prepareMaxResultBuffer() {
        localConnectionString = new String(connectionString);
    }

    /**
     * Create TEST_TABLE with 1 column nchar(precision) with numberOfRows. Let's calculate payload on example:
     * numberOfRows = 800, precision = 10
     *
     * Payload (in Bytes) = 49 (Header plus column metadata) + numberOfRows * (precision * 2 + 1 + 2) (3 extra bytes are
     * for column length and end of line character)
     *
     * So payload generated by this method = 49 + 800 * (10 * 2 + 2 + 1) = 49 + 800 * 23 = 18449
     *
     * Default packetLength = 8000, so payload is sent in 3 packets
     *
     * @throws SQLException
     *         Signalizes error when creating TEST_TABLE
     */
    @BeforeAll
    static void createAndPopulateNCharTestTable() throws Exception {
        // reset logging to avoid severe logs triggered from tests
        LogManager.getLogManager().reset();

        setConnection();

        String insertSQL = "INSERT INTO " + TEST_TABLE_NAME + " VALUES (?)";
        int numberOfRows = 800;
        int precision = 10;

        try (Connection connection = DriverManager.getConnection(connectionString);
                Statement statement = connection.createStatement();
                PreparedStatement preparedStatement = connection.prepareStatement(insertSQL)) {

            // drop Table if exists and then create new one
            TestUtils.dropTableIfExists(TEST_TABLE_NAME, statement);
            statement.execute("CREATE TABLE " + TEST_TABLE_NAME + " ( col1 nchar(" + precision + "))");

            // insert into Table
            for (int i = 0; i < numberOfRows; i++) {
                preparedStatement.setString(1, generateRandomString(precision));
                preparedStatement.addBatch();
            }
            preparedStatement.executeBatch();
        }
    }

    @AfterAll
    static void teardownTestTable() throws SQLException {
        try (Statement statement = connection.createStatement()) {
            TestUtils.dropTableIfExists(TEST_TABLE_NAME, statement);
        }
    }

    /**
     * Test shows influence of MaxResultBuffer on all types of ResultSet (considering state of response buffering). In
     * this test, maxResultBuffer is not affecting the work of driver in any way.
     * 
     * @param maxResultBuffer
     *        value of MaxResultBuffer parameter provided by source method
     * @param adaptiveBuffering
     *        value of responseBuffering provided by source method
     * @param resultSetType
     *        type of ResultSet provided by source method
     * @param concurrencyMode
     *        type of ResultSet's concurrency provided by source method
     */
    @Tag(Constants.xAzureSQLDW)
    @ParameterizedTest(
            name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}, resultSetType = {2}, concurrencyMode = {3}")
    @MethodSource("linearResultSetData")
    void testResultSetLinear(String maxResultBuffer, boolean adaptiveBuffering, int resultSetType,
            int concurrencyMode) {
        setResponseBufferingAdaptive(adaptiveBuffering);
        setMaxResultBuffer(maxResultBuffer);
        try {
            resultSet(resultSetType, concurrencyMode);
        } catch (SQLException e) {
            fail();
        }
    }

    private static Iterable<Object[]> linearResultSetData() {
        return Arrays.asList(new Object[][] {
                // maxResultBuffer set to 5k
                {"5k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"5k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"5k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"5k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                {"5k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"5k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"5k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"5k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                // maxResultBuffer set to 10k
                {"10k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                {"10k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"10k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"10k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"10k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                {"10k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"10k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"10k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"10k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                // max ResultBuffer set to 15k
                {"15k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                {"15k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"15k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"15k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"15k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                {"15k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"15k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"15k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"15k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                // maxResultBuffer set to 17k
                {"17k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                {"17k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"17k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"17k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"17k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                {"17k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"17k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"17k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"17k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                // maxResultBuffer set to 20k
                {"20k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                {"20k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"20k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"20k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"20k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                {"20k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                {"20k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"20k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"20k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"20k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},});
    }

    /**
     * Test shows influence of MaxResultBuffer on all types of ResultSet (considering state of response buffering). In
     * this test, driver is throwing exception because MaxResultBuffer property was exceeded.
     *
     * @param maxResultBuffer
     *        value of MaxResultBuffer parameter provided by source method
     * @param adaptiveBuffering
     *        value of responseBuffering connection property provided by source method
     * @param resultSetType
     *        type of ResultSet provided by source method
     * @param concurrencyMode
     *        type of ResultSet's concurrency provided by source method
     */
    @ParameterizedTest(
            name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}, resultSetType = {2}, concurrencyMode = {3}")
    @MethodSource("linearResultSetDataThrowsSQLException")
    void testResultSetLinearThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering, int resultSetType,
            int concurrencyMode) {
        setResponseBufferingAdaptive(adaptiveBuffering);
        setMaxResultBuffer(maxResultBuffer);

        Assertions.assertThrows(SQLServerException.class, () -> resultSet(resultSetType, concurrencyMode));
    }

    private static Iterable<Object[]> linearResultSetDataThrowsSQLException() {
        return Arrays.asList(new Object[][] {
                // maxResultBuffer set to 3k
                {"3k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                {"3k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"3k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"3k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"3k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                {"3k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                {"3k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE},
                {"3k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"3k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY},
                {"3k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},
                // maxResultBuffer set to 5k
                {"5k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                {"5k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                // maxResultBuffer set to 10k
                {"10k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                // maxResultBuffer set to 15k
                {"15k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},
                // maxResultBuffer set to 17k
                {"17k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},});
    }

    /**
     * Test shows influence of MaxResultBuffer when multiple statements are executed, which result in multiple
     * ResultSets (considering state of response buffering).
     * 
     * @param maxResultBuffer
     *        value of MaxResultBuffer parameter provided by source method
     * @param adaptiveBuffering
     *        value of responseBuffering connection property provided by source method
     */
    @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}")
    @MethodSource("preparedStatementData")
    void testPreparedStatementMultipleResultSets(String maxResultBuffer, boolean adaptiveBuffering) {
        setResponseBufferingAdaptive(adaptiveBuffering);
        setMaxResultBuffer(maxResultBuffer);
        try {
            preparedStatementWithMultipleResultSets();
        } catch (SQLException e) {
            fail();
        }
    }

    private static Iterable<Object[]> preparedStatementData() {
        return Arrays.asList(new Object[][] {{"20k", true}, {"20k", false},});
    }

    /**
     * Test shows influence of MaxResultBuffer when multiple statements are executed, which result in multiple
     * ResultSets (considering state of response buffering). In this test, driver is throwing exception because
     * MaxResultBuffer property was exceeded.
     *
     * @param maxResultBuffer
     *        value of MaxResultBuffer parameter provided by source method
     * @param adaptiveBuffering
     *        value of responseBuffering connection property provided by source method
     */
    @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}")
    @MethodSource("preparedStatementDataThrowsSQLException")
    void testPreparedStatementMultipleResultSetsThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering) {
        setResponseBufferingAdaptive(adaptiveBuffering);
        setMaxResultBuffer(maxResultBuffer);

        Assertions.assertThrows(SQLServerException.class, this::preparedStatementWithMultipleResultSets);
    }

    private static Iterable<Object[]> preparedStatementDataThrowsSQLException() {
        return Arrays.asList(new Object[][] {
                // maxResultBuffer set to 3k
                {"3k", true}, {"3k", false},
                // maxResultBuffer set to 5k
                {"5k", true}, {"5k", false},
                // maxResultBuffer set to 10k
                {"10k", true}, {"10k", false},
                // maxResultBuffer set to 15k
                {"15k", true}, {"15k", false},
                // maxResultBuffer set to 17k
                {"17k", true}, {"17k", false},});
    }

    /**
     * Test shows influence of MaxResultBuffer when multiple statements are executed in one call (considering state of
     * response buffering).
     *
     * @param maxResultBuffer
     *        value of MaxResultBuffer parameter provided by source method
     * @param adaptiveBuffering
     *        value of responseBuffering connection property provided by source method
     */
    @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}")
    @MethodSource("twoQueriesData")
    void testTwoQueriesInOneStatement(String maxResultBuffer, boolean adaptiveBuffering) {
        setResponseBufferingAdaptive(adaptiveBuffering);
        setMaxResultBuffer(maxResultBuffer);
        try {
            twoQueriesInOneStatement();
        } catch (SQLException e) {
            fail();
        }
    }

    private static Iterable<Object[]> twoQueriesData() {
        return Arrays.asList(new Object[][] {{"10k", true}, {"15k", true}, {"17k", true}, {"20k", true},});
    }

    /**
     * Test shows influence of MaxResultBuffer when multiple statements are executed in one call (considering state of
     * response buffering).In this test, driver is throwing exception because MaxResultBuffer property was exceeded.
     *
     * @param maxResultBuffer
     *        value of MaxResultBuffer parameter provided by source method
     * @param adaptiveBuffering
     *        value of responseBuffering connection property provided by source method
     */
    @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}")
    @MethodSource("twoQueriesDataThrowsSQLException")
    void testTwoQueriesInOneStatementThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering) {
        setResponseBufferingAdaptive(adaptiveBuffering);
        setMaxResultBuffer(maxResultBuffer);

        Assertions.assertThrows(SQLServerException.class, this::twoQueriesInOneStatement);
    }

    private static Iterable<Object[]> twoQueriesDataThrowsSQLException() {
        return Arrays.asList(new Object[][] {
                // maxResultBuffer set to 3k
                {"3k", true}, {"3k", false},
                // maxResultBuffer set to 5k
                {"5k", true}, {"5k", false},
                // maxResultBuffer set to 10k
                {"10k", false},
                // maxResultBuffer set to 15k
                {"15k", false},
                // maxResultBuffer set to 17k
                {"17k", false},
                // maxResultBuffer set to 20k
                {"20k", false},});
    }

    /**
     * This method tests if all packets from ResultSet are correctly retrieved
     *
     * @param resultSetType
     *        Result set type; one of <code>ResultSet.TYPE_FORWARD_ONLY</code>,
     *        <code>ResultSet.TYPE_SCROLL_INSENSITIVE</code>, or <code>ResultSet.TYPE_SCROLL_SENSITIVE</code>
     * @param concurrencyMode
     *        Concurrency type; one of <code>ResultSet.CONCUR_READ_ONLY</code> or
     *        <code>ResultSet.CONCUR_UPDATABLE</code>
     *
     * @throws SQLException
     *         Exception is thrown when maxResultBuffer is exceeded
     */
    private void resultSet(int resultSetType, int concurrencyMode) throws SQLException {
        try (Connection connection = DriverManager.getConnection(localConnectionString);
                Statement statement = connection.createStatement(resultSetType, concurrencyMode)) {
            statement.execute("SELECT * FROM " + TEST_TABLE_NAME);
            try (ResultSet resultSet = statement.getResultSet()) {
                while (resultSet.next()) {}
            }
        }
    }

    /**
     * This method tests if Statements are detached properly, when first one hasn't been completely retrieved and second
     * one have been executed.
     *
     * @throws SQLException
     *         Exception is thrown when maxResultBuffer is exceeded
     */
    private void preparedStatementWithMultipleResultSets() throws SQLException {
        String selectSQL = "SELECT * FROM " + TEST_TABLE_NAME;

        try (Connection connection = DriverManager.getConnection(localConnectionString);
                PreparedStatement statement = connection.prepareStatement(selectSQL);
                ResultSet resultSet = statement.executeQuery()) {

            try (PreparedStatement secondStatement = connection.prepareStatement(selectSQL);
                    ResultSet secondResultSet = secondStatement.executeQuery()) {
                while (resultSet.next()) {}

                try (PreparedStatement thirdStatement = connection.prepareStatement(selectSQL);
                        ResultSet thirdResultSet = thirdStatement.executeQuery()) {
                    while (thirdResultSet.next()) {}
                    while (secondResultSet.next()) {}
                }
            }
        }
    }

    /**
     * This method tests if ResultSet's are retrieved correctly, when more than one Query is executed inside single
     * statement
     * 
     * @throws SQLException
     *         Exception is thrown when maxResultBuffer is exceeded
     */
    private void twoQueriesInOneStatement() throws SQLException {
        try (Connection connection = DriverManager.getConnection(localConnectionString);
                Statement statement = connection.createStatement()) {
            statement.execute("SELECT * FROM " + TEST_TABLE_NAME + ";SELECT * FROM " + TEST_TABLE_NAME);

            try (ResultSet resultSet = statement.getResultSet()) {
                while (resultSet.next()) {}
            }

            if (statement.getMoreResults()) {
                try (ResultSet totallyNewResultSet = statement.getResultSet()) {
                    while (totallyNewResultSet.next()) {}
                }
            }
        }
    }

    private static String generateRandomString(int precision) {
        int leftLimit = 33;
        int rightLimit = 126;
        Random random = new Random();
        return random.ints(leftLimit, rightLimit).limit(precision)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
    }

    private static void setResponseBufferingAdaptive(boolean adaptive) {
        String value = adaptive ? "adaptive" : "full";
        localConnectionString = TestUtils.addOrOverrideProperty(localConnectionString, "responseBuffering", value);
        AbstractTest.updateDataSource(localConnectionString, ds);
    }

    private static void setMaxResultBuffer(String maxResultBuffer) {
        localConnectionString = TestUtils.addOrOverrideProperty(localConnectionString, "maxResultBuffer",
                maxResultBuffer);
        AbstractTest.updateDataSource(localConnectionString, ds);
    }
}
